package com.ctw.tinyservices.id.core.snowflake.holder;

import com.alibaba.fastjson.JSON;
import com.ctw.tinyservices.id.common.lifecycle.AbstractIdLifecycle;
import com.ctw.tinyservices.id.common.utils.IpUtils;
import com.ctw.tinyservices.id.common.utils.LogUtils;
import com.ctw.tinyservices.id.common.utils.PropertiesUtils;
import com.ctw.tinyservices.id.core.snowflake.exception.CheckLastTimeException;
import com.ctw.tinyservices.id.core.snowflake.exception.LocalWorkIdNotFoundException;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @author TongWei.Chen 2022/4/8 16:03
 *
 * AbstractIdSnowflakeHolder
 **/
public abstract class AbstractIdSnowflakeHolder extends AbstractIdLifecycle implements IdSnowflakeHolder {

    protected String ip;
    protected int port;
    // 保存自身的key ip:port
    protected String listenAddress;
    protected int workerId;
    protected long lastUpdateTime;
    protected ScheduledExecutorService scheduledExecutorService = null;

    private static final String MAX_TIMESTAMP_KEY = "maxTimestamp";
    private static final String WORKER_ID_KEY = "workerId";
    private static final String LOCAL_FILE_PORT_KEY = "{port}";
    private static final String LOCAL_FILE_PATH =
            System.getProperty("java.io.tmpdir")
                    + File.separator
                    + "tinyservices_id/config/snowflake/"
                    + LOCAL_FILE_PORT_KEY
                    + "/WORKER_ID.properties";

    public AbstractIdSnowflakeHolder(int port) {
        this.ip = IpUtils.getIp();
        this.port = port;
        this.listenAddress = ip + ":" + port;
    }

    protected void doUpdateMaxTimestampException() {}
    public boolean isAlive() {
        return true;
    }
    protected abstract void doProcess() throws Exception;
    protected abstract void doUpdateMaxTimestamp() throws Exception;

    @Override
    public int getWorkerId() {
        return this.workerId;
    }

    @Override
    protected void doInit() {
        try {
            doProcess();
            // workerId默认是0
            updateLocalWorkerId();
        } catch (Exception e) {
            doProcessException(e);
        }
    }

    @Override
    protected void doStart() {
        LogUtils.info(AbstractIdSnowflakeHolder.class, "doStart data begin...");
        scheduledUploadData();
        doStartOtherProcess();
        LogUtils.info(AbstractIdSnowflakeHolder.class, "doStart data end...");
    }

    protected void doStartOtherProcess() {}

    @Override
    protected void doStop() {
        LogUtils.info(AbstractIdSnowflakeHolder.class, "doStop data begin...");
        scheduledExecutorService.shutdownNow();
        LogUtils.info(AbstractIdSnowflakeHolder.class, "doStop data end...");
    }

    @Override
    public String name() {
        return "AbstractIdSnowflakeHolder";
    }

    protected void updateLocalWorkerId() {
        File localFile = new File(LOCAL_FILE_PATH.replace(LOCAL_FILE_PORT_KEY, String.valueOf(port)));
        boolean exists = localFile.exists();
        LogUtils.info(AbstractIdSnowflakeHolder.class,"file path is {}, file exists status is {}", localFile.getPath(), exists);
        String data = new StringBuilder(WORKER_ID_KEY).append("=").append(workerId).append("\n").append(MAX_TIMESTAMP_KEY).append("=").append(System.currentTimeMillis()).toString();
        if (exists) {
            try {
                Files.write(data.getBytes(StandardCharsets.UTF_8), localFile);
                LogUtils.info(AbstractIdSnowflakeHolder.class,"update file cache workerId is {}", workerId);
            } catch (IOException e) {
                LogUtils.error(AbstractIdSnowflakeHolder.class,"update file cache error ", e);
            }
        } else {
            // 不存在文件 create
            try {
                boolean mkdirs = localFile.getParentFile().mkdirs();
                LogUtils.info(AbstractIdSnowflakeHolder.class, "init local file cache create parent dir status is {}, workerId is {}", mkdirs, workerId);
                if (! localFile.exists()) {
                    if (localFile.createNewFile()) {
                        Files.write(data.getBytes(StandardCharsets.UTF_8), localFile);
                        LogUtils.info(AbstractIdSnowflakeHolder.class,"local file cache workerId is {}", workerId);
                    }
                }
            } catch (IOException e) {
                LogUtils.warn(AbstractIdSnowflakeHolder.class,"create workerId conf file error", e);
            }
        }
    }

    protected void scheduledUploadData() {
        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread thread = new Thread(r, "schedule-upload-time");
            thread.setDaemon(true);
            return thread;
        });
        scheduledExecutorService.scheduleWithFixedDelay(() -> updateMaxTimestamp(), 1L, 3L, TimeUnit.SECONDS);
    }

    protected void updateMaxTimestamp() {
        try {
            if (System.currentTimeMillis() < lastUpdateTime) {
                return;
            }
            doUpdateMaxTimestamp();
            updateLocalWorkerId();
            lastUpdateTime = System.currentTimeMillis();
        } catch (Exception e) {
            LogUtils.error(AbstractIdSnowflakeHolder.class, "update maxTimestamp to db error, workerId is {} error is {}", workerId, e);
            doUpdateMaxTimestampException();
        }
    }

    protected void doProcessException(Exception e) {
        LogUtils.error(AbstractIdSnowflakeHolder.class, "doInit error, and use local file ", e);
        String localFilePath = LOCAL_FILE_PATH.replace(LOCAL_FILE_PORT_KEY, String.valueOf(port));
        try {
            Properties properties = PropertiesUtils.load(new FileInputStream(localFilePath));
            Long maxTimestamp = Long.valueOf(properties.getProperty(MAX_TIMESTAMP_KEY));
            if (maxTimestamp != null && System.currentTimeMillis() < maxTimestamp) {
                throw new CheckLastTimeException("init timestamp check error,forever node timestamp gt this node time");
            }
            workerId = Integer.valueOf(properties.getProperty(WORKER_ID_KEY));
            LogUtils.warn(AbstractIdSnowflakeHolder.class, "Init FAILED ,use local node file properties workerId:{}", workerId);
        } catch (Exception e1) {
            LogUtils.error(AbstractIdSnowflakeHolder.class, "Read local file error ", e1);
            throw new LocalWorkIdNotFoundException("local file: " + localFilePath + " maybe not exists");
        }
    }

    /**
     * 构建需要上传的数据
     *
     * @return
     */
    protected String buildData() {
        return buildData(this.workerId);
    }

    protected String buildData(int workerId) {
        return JSON.toJSONString(new Endpoint(workerId, ip, port, System.currentTimeMillis()));
    }

    protected Endpoint deBuildData(String json) {
        return JSON.parseObject(json, Endpoint.class);
    }

    /**
     * 上报数据结构
     */
    static class Endpoint {
        private int workerId;
        private String ip;
        private int port;
        private long timestamp;

        public Endpoint() {
        }

        public Endpoint(int workerId, String ip, int port, long timestamp) {
            this.workerId = workerId;
            this.ip = ip;
            this.port = port;
            this.timestamp = timestamp;
        }

        public int getWorkerId() {
            return workerId;
        }

        public void setWorkerId(int workerId) {
            this.workerId = workerId;
        }

        public String getIp() {
            return ip;
        }

        public void setIp(String ip) {
            this.ip = ip;
        }

        public int getPort() {
            return port;
        }

        public void setPort(int port) {
            this.port = port;
        }

        public long getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }
    }
}
